/*
 * Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.font;

import java.util.Locale;

import sun.awt.SunHints;
import sun.awt.SunToolkit;
import sun.util.logging.PlatformLogger;

/**
 * Small utility class to manage FontConfig.
 */
public class FontConfigManager {

	static boolean fontConfigFailed = false;

	/* This is populated by native */
	private static final FontConfigInfo fcInfo = new FontConfigInfo();

	/* Begin support for GTK Look and Feel - query libfontconfig and
	 * return a composite Font to Swing that uses the desktop font(s).
	 */

	/* These next three classes are just data structures.
	 */
	public static class FontConfigFont {
		public String familyName;        // eg Bitstream Vera Sans
		public String styleStr;          // eg Bold
		public String fullName;          // eg Bitstream Vera Sans Bold
		public String fontFile;          // eg /usr/X11/lib/fonts/foo.ttf
	}

	public static class FcCompFont {
		public String fcName;            // eg sans
		public String fcFamily;          // eg sans
		public String jdkName;           // eg sansserif
		public int style;                // eg 0=PLAIN
		public FontConfigFont firstFont;
		public FontConfigFont[] allFonts;
		//boolean preferBitmaps;    // if embedded bitmaps preferred over AA
		public CompositeFont compFont;   // null if not yet created/known.
	}

	public static class FontConfigInfo {
		public int fcVersion;
		public String[] cacheDirs = new String[4];
	}

	/* fontconfig recognises slants roman, italic, as well as oblique,
	 * and a slew of weights, where the ones that matter here are
	 * regular and bold.
	 * To fully qualify what we want, we can for example ask for (eg)
	 * Font.PLAIN             : "serif:regular:roman"
	 * Font.BOLD              : "serif:bold:roman"
	 * Font.ITALIC            : "serif:regular:italic"
	 * Font.BOLD|Font.ITALIC  : "serif:bold:italic"
	 */
	private static String[] fontConfigNames = {
			"sans:regular:roman",
			"sans:bold:roman",
			"sans:regular:italic",
			"sans:bold:italic",

			"serif:regular:roman",
			"serif:bold:roman",
			"serif:regular:italic",
			"serif:bold:italic",

			"monospace:regular:roman",
			"monospace:bold:roman",
			"monospace:regular:italic",
			"monospace:bold:italic",
	};

	/* This array has the array elements created in Java code and is
	 * passed down to native to be filled in.
	 */
	private FcCompFont[] fontConfigFonts;

	/**
	 * Instantiates a new FontConfigManager getting the default instance
	 * of FontManager from the FontManagerFactory.
	 */
	public FontConfigManager() {
	}

	/* Called from code that needs to know what are the AA settings
	 * that apps using FC would pick up for the default desktop font.
	 * Note apps can change the default desktop font. etc, so this
	 * isn't certain to be right but its going to correct for most cases.
	 * Native return values map to the text aa values in sun.awt.SunHints.
	 * which is used to look up the renderinghint value object.
	 */
	public static Object getFontConfigAAHint() {
		return getFontConfigAAHint("sans");
	}

	/* This is public solely so that for debugging purposes it can be called
	 * with other names, which might (eg) include a size, eg "sans-24"
	 * The return value is a text aa rendering hint value.
	 * Normally we should call the no-args version.
	 */
	public static Object getFontConfigAAHint(String fcFamily) {
		if (FontUtilities.isWindows) {
			return null;
		} else {
			int hint = getFontConfigAASettings(getFCLocaleStr(), fcFamily);
			if (hint < 0) {
				return null;
			} else {
				return SunHints.Value.get(SunHints.INTKEY_TEXT_ANTIALIASING,
						hint);
			}
		}
	}


	private static String getFCLocaleStr() {
		Locale l = SunToolkit.getStartupLocale();
		String localeStr = l.getLanguage();
		String country = l.getCountry();
		if (!country.equals("")) {
			localeStr = localeStr + "-" + country;
		}
		return localeStr;
	}

	/* This does cause the native libfontconfig to be loaded and unloaded,
	 * but it does not incur the overhead of initialisation of its
	 * data structures, so shouldn't have a measurable impact.
	 */
	public static native int getFontConfigVersion();

	/* This can be made public if it's needed to force a re-read
	 * rather than using the cached values. The re-read would be needed
	 * only if some event signalled that the fontconfig has changed.
	 * In that event this method would need to return directly the array
	 * to be used by the caller in case it subsequently changed.
	 */
	public synchronized void initFontConfigFonts(boolean includeFallbacks) {

		if (fontConfigFonts != null) {
			if (!includeFallbacks || (fontConfigFonts[0].allFonts != null)) {
				return;
			}
		}

		if (FontUtilities.isWindows || fontConfigFailed) {
			return;
		}

		long t0 = 0;
		if (FontUtilities.isLogging()) {
			t0 = System.nanoTime();
		}

		FcCompFont[] fontArr = new FcCompFont[fontConfigNames.length];

		for (int i = 0; i< fontArr.length; i++) {
			fontArr[i] = new FcCompFont();
			fontArr[i].fcName = fontConfigNames[i];
			int colonPos = fontArr[i].fcName.indexOf(':');
			fontArr[i].fcFamily = fontArr[i].fcName.substring(0, colonPos);
			fontArr[i].jdkName = FontUtilities.mapFcName(fontArr[i].fcFamily);
			fontArr[i].style = i % 4; // depends on array order.
		}
		getFontConfig(getFCLocaleStr(), fcInfo, fontArr, includeFallbacks);
		FontConfigFont anyFont = null;
		/* If don't find anything (eg no libfontconfig), then just return */
		for (int i = 0; i< fontArr.length; i++) {
			FcCompFont fci = fontArr[i];
			if (fci.firstFont == null) {
				if (FontUtilities.isLogging()) {
					PlatformLogger logger = FontUtilities.getLogger();
					logger.info("Fontconfig returned no font for " +
							fontArr[i].fcName);
				}
				fontConfigFailed = true;
			} else if (anyFont == null) {
				anyFont = fci.firstFont;
			}
		}

		if (anyFont == null) {
			if (FontUtilities.isLogging()) {
				PlatformLogger logger = FontUtilities.getLogger();
				logger.info("Fontconfig returned no fonts at all.");
			}
			fontConfigFailed = true;
			return;
		} else if (fontConfigFailed) {
			for (int i = 0; i< fontArr.length; i++) {
				if (fontArr[i].firstFont == null) {
					fontArr[i].firstFont = anyFont;
				}
			}
		}

		fontConfigFonts = fontArr;

		if (FontUtilities.isLogging()) {

			PlatformLogger logger = FontUtilities.getLogger();

			long t1 = System.nanoTime();
			logger.info("Time spent accessing fontconfig="
					+ ((t1 - t0) / 1000000) + "ms.");

			for (int i = 0; i< fontConfigFonts.length; i++) {
				FcCompFont fci = fontConfigFonts[i];
				logger.info("FC font " + fci.fcName+" maps to family " +
						fci.firstFont.familyName +
						" in file " + fci.firstFont.fontFile);
				if (fci.allFonts != null) {
					for (int f=0;f<fci.allFonts.length;f++) {
						FontConfigFont fcf = fci.allFonts[f];
						logger.info("Family=" + fcf.familyName +
								" Style="+ fcf.styleStr +
								" Fullname="+fcf.fullName +
								" File="+fcf.fontFile);
					}
				}
			}
		}
	}

	public PhysicalFont registerFromFcInfo(FcCompFont fcInfo) {

		SunFontManager fm = SunFontManager.getInstance();

		/* If it's a TTC file we need to know that as we will need to
		 * make sure we return the right font */
		String fontFile = fcInfo.firstFont.fontFile;
		int offset = fontFile.length()-4;
		if (offset <= 0) {
			return null;
		}
		String ext = fontFile.substring(offset).toLowerCase();
		boolean isTTC = ext.equals(".ttc");

		/* If this file is already registered, can just return its font.
		 * However we do need to check in case it's a TTC as we need
		 * a specific font, so rather than directly returning it, let
		 * findFont2D resolve that.
		 */
		PhysicalFont physFont = fm.getRegisteredFontFile(fontFile);
		if (physFont != null) {
			if (isTTC) {
				Font2D f2d = fm.findFont2D(fcInfo.firstFont.familyName,
						fcInfo.style,
						FontManager.NO_FALLBACK);
				if (f2d instanceof PhysicalFont) { /* paranoia */
					return (PhysicalFont)f2d;
				} else {
					return null;
				}
			} else {
				return physFont;
			}
		}

		/* If the font may hide a JRE font (eg fontconfig says it is
		 * Lucida Sans), we want to use the JRE version, so make it
		 * point to the JRE font.
		 */
		physFont = fm.findJREDeferredFont(fcInfo.firstFont.familyName,
				fcInfo.style);

		/* It is also possible the font file is on the "deferred" list,
		 * in which case we can just initialise it now.
		 */
		if (physFont == null &&
				fm.isDeferredFont(fontFile) == true) {
			physFont = fm.initialiseDeferredFont(fcInfo.firstFont.fontFile);
			/* use findFont2D to get the right font from TTC's */
			if (physFont != null) {
				if (isTTC) {
					Font2D f2d = fm.findFont2D(fcInfo.firstFont.familyName,
							fcInfo.style,
							FontManager.NO_FALLBACK);
					if (f2d instanceof PhysicalFont) { /* paranoia */
						return (PhysicalFont)f2d;
					} else {
						return null;
					}
				} else {
					return physFont;
				}
			}
		}

		/* In the majority of cases we reach here, and need to determine
		 * the type and rank to register the font.
		 */
		if (physFont == null) {
			int fontFormat = SunFontManager.FONTFORMAT_NONE;
			int fontRank = Font2D.UNKNOWN_RANK;

			if (ext.equals(".ttf") || isTTC) {
				fontFormat = SunFontManager.FONTFORMAT_TRUETYPE;
				fontRank = Font2D.TTF_RANK;
			} else if (ext.equals(".pfa") || ext.equals(".pfb")) {
				fontFormat = SunFontManager.FONTFORMAT_TYPE1;
				fontRank = Font2D.TYPE1_RANK;
			}
			physFont = fm.registerFontFile(fcInfo.firstFont.fontFile, null,
					fontFormat, true, fontRank);
		}
		return physFont;
	}

	/*
	 * We need to return a Composite font which has as the font in
	 * its first slot one obtained from fontconfig.
	 */
	public CompositeFont getFontConfigFont(String name, int style) {

		name = name.toLowerCase();

		initFontConfigFonts(false);
		if (fontConfigFonts == null) {
			// This avoids an immediate NPE if fontconfig look up failed
			// but doesn't guarantee this is a recoverable situation.
			return null;
		}

		FcCompFont fcInfo = null;
		for (int i=0; i<fontConfigFonts.length; i++) {
			if (name.equals(fontConfigFonts[i].fcFamily) &&
					style == fontConfigFonts[i].style) {
				fcInfo = fontConfigFonts[i];
				break;
			}
		}
		if (fcInfo == null) {
			fcInfo = fontConfigFonts[0];
		}

		if (FontUtilities.isLogging()) {
			FontUtilities.getLogger()
					.info("FC name=" + name + " style=" + style +
							" uses " + fcInfo.firstFont.familyName +
							" in file: " + fcInfo.firstFont.fontFile);
		}

		if (fcInfo.compFont != null) {
			return fcInfo.compFont;
		}

		/* jdkFont is going to be used for slots 1..N and as a fallback.
		 * Slot 0 will be the physical font from fontconfig.
		 */
		FontManager fm = FontManagerFactory.getInstance();
		CompositeFont jdkFont = (CompositeFont)
				fm.findFont2D(fcInfo.jdkName, style, FontManager.LOGICAL_FALLBACK);

		if (fcInfo.firstFont.familyName == null ||
				fcInfo.firstFont.fontFile == null) {
			return (fcInfo.compFont = jdkFont);
		}

		/* First, see if the family and exact style is already registered.
		 * If it is, use it. If it's not, then try to register it.
		 * If that registration fails (signalled by null) just return the
		 * regular JDK composite.
		 * Algorithmically styled fonts won't match on exact style, so
		 * will fall through this code, but the regisration code will
		 * find that file already registered and return its font.
		 */
		FontFamily family = FontFamily.getFamily(fcInfo.firstFont.familyName);
		PhysicalFont physFont = null;
		if (family != null) {
			Font2D f2D = family.getFontWithExactStyleMatch(fcInfo.style);
			if (f2D instanceof PhysicalFont) {
				physFont = (PhysicalFont)f2D;
			}
		}

		if (physFont == null ||
				!fcInfo.firstFont.fontFile.equals(physFont.platName)) {
			physFont = registerFromFcInfo(fcInfo);
			if (physFont == null) {
				return (fcInfo.compFont = jdkFont);
			}
			family = FontFamily.getFamily(physFont.getFamilyName(null));
		}

		/* Now register the fonts in the family (the other styles) after
		 * checking that they aren't already registered and are actually in
		 * a different file. They may be the same file in CJK cases.
		 * For cases where they are different font files - eg as is common for
		 * Latin fonts, then we rely on fontconfig to report these correctly.
		 * Assume that all styles of this font are found by fontconfig,
		 * so we can find all the family members which must be registered
		 * together to prevent synthetic styling.
		 */
		for (int i=0; i<fontConfigFonts.length; i++) {
			FcCompFont fc = fontConfigFonts[i];
			if (fc != fcInfo &&
					physFont.getFamilyName(null).equals(fc.firstFont.familyName) &&
					!fc.firstFont.fontFile.equals(physFont.platName) &&
					family.getFontWithExactStyleMatch(fc.style) == null) {

				registerFromFcInfo(fontConfigFonts[i]);
			}
		}

		/* Now we have a physical font. We will back this up with the JDK
		 * logical font (sansserif, serif, or monospaced) that corresponds
		 * to the Pango/GTK/FC logical font name.
		 */
		return (fcInfo.compFont = new CompositeFont(physFont, jdkFont));
	}

	/**
	 *
	 * @param locale
	 * @param fcFamily
	 * @return
	 */
	public FcCompFont[] getFontConfigFonts() {
		return fontConfigFonts;
	}

	/* Return an array of FcCompFont structs describing the primary
	 * font located for each of fontconfig/GTK/Pango's logical font names.
	 */
	private static native void getFontConfig(String locale,
											 FontConfigInfo fcInfo,
											 FcCompFont[] fonts,
											 boolean includeFallbacks);

	void populateFontConfig(FcCompFont[] fcInfo) {
		fontConfigFonts = fcInfo;
	}

	FcCompFont[] loadFontConfig() {
		initFontConfigFonts(true);
		return fontConfigFonts;
	}

	FontConfigInfo getFontConfigInfo() {
		initFontConfigFonts(true);
		return fcInfo;
	}

	private static native int
	getFontConfigAASettings(String locale, String fcFamily);
}